android so层解密分析

参考

Android so层逆向分析入门 - Curz0n’s Blog

Android脱壳工具整理_apk脱壳工具-CSDN博客

ida为9版本

so分析

静态注册的方法可以直接在导出表查询到 动态注册则是需要查看jni_onload函数进行分析

image-20250323122738595

stringFromJNI为静态注册

test方法为动态注册

image-20250323122033147

jni_onload第一个参数的类型是JavaVM* 面对错误可以手动修复

动态注册流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//第一步,实现JNI_OnLoad方法
JNIEXPORT jint JNI_OnLoad(JavaVM* jvm, void* reserved){
//第二步,获取JNIEnv
JNIEnv* env = NULL;
if(jvm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK){
return JNI_FALSE;
}
//第三步,获取注册方法所在Java类的引用
jclass clazz = env->FindClass("com/curz0n/MainActivity");
if (!clazz){
return JNI_FALSE;
}
//第四步,动态注册native方法
if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0]))){
return JNI_FALSE;
}
return JNI_VERSION_1_6;
}

gMethods为JNINativeMethod结构体

1
2
3
4
5
typedef struct {
const char* name; //动态注册的Java方法名
const char* signature; //描述方法参数和返回值
void* fnPtr; //指向实现Java方法的C/C++函数指针
} JNINativeMethod;

跟进第一个函数 同样修改数据类型

image-20250323131520804

跟进第二个函数 修改a1变量 由于a1是第一个函数的返回值 所以是jnienv的数据类型

image-20250323132353961

那么对应的第三个函数就是注册函数 再次修复

image-20250323132739988

image-20250323132822717

init段

在链接so共享目标文件的时候,如果so中存在.init和.init_array段,则会先执行.init和.init_array段的函数,然后再执行JNI_OnLoad函数。通过静态分析可知,JNI_OnLoad函数中的v4指针指向的地址上的变量值是加密状态,在实际运行的过程中,v4指针指向的地址上的值应该是解密状态,所以解密的操作应该在JNI_OnLoad函数运行之前,.init或者.init_array段上的函数。
查看Segments视图(快捷键Ctrl+S),该目标文件只存在.init_array段:

image-20250323133010343

发现异或的东西 要么调试so文件 要么手动修

这里可以解出注册方法在apk中的test方法

JNINativeMethod结构体的第三个成员指向实现Java方法的C/C++函数地址,so文件的.data段一般是保存已经初始化的全局静态变量和局部变量,动态注册函数的信息一般存放在.data.rel.ro.local段。在IDA View视图选中byte_1C066或者byte_1C070变量,交叉引用(快捷键X)跳转到.data.rel.ro段

image-20250323140052348

image-20250323140120072

跟进找到一个处理函数

image-20250323140306591

继续跟进 这里的format参数也是需要进行解密才能知道是什么

image-20250323140522590

1
2
3
4
5
data=[0xDA, 0x85, 0x87, 0x9A, 0x96, 0xDA, 0xD0, 0x91, 0xDA, 0x98,
0x94, 0x85, 0x86, 0xF5]
for i in data:
print(chr(i^0xf5), end='')
# /proc/%d/maps

image-20250323141649526

根据解密以后的字符串可以知道这个地方就是获取libnative-lib.so的加载地址

25

将获取到的加载地址当作返回值

image-20250323144349760

这里就要设计到elf文件的文件结构了 使用010打开加载模板

image-20250323145001405
1
v12 = (baseaddres + *(baseaddres + 28));

获取到获取程序头表偏移值52 加载加载的基地址 就是

image-20250323145404879

ELF文件的struct program_header_table(程序头表)是ELF文件结构中的一个重要组成部分,它定义了程序的“执行时视图”,用于指导加载器如何将程序的各个段加载到内存中。

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;

程序头表中的每个表项是一个ElfW(Phdr)结构体,其具体字段如下:

  • p_type:段的类型,如PT_LOAD表示该段需要被加载到内存。
  • p_flags:段的标志,如PF_R表示可读,PF_W表示可写,PF_X表示可执行。
  • p_offset:段在文件中的偏移量。
  • p_vaddr:段在内存中的虚拟地址。
  • p_paddr:段在物理内存中的地址(通常在现代操作系统中不使用)。
  • p_filesz:段在文件中的大小。
  • p_memsz:段在内存中的大小。
  • p_align:段的对齐方式。

到了

image-20250323150343900

Elf32_Half e_phnum是一个非常重要的字段,它表示程序头表(Program Header Table)中表项的数量 程序头表描述了ELF文件的各个段(segment),这些段是加载器在加载程序到内存时需要处理的单元。e_phnum的值告诉加载器程序头表中有多少个段描述符,加载器需要根据这个数量来遍历整个程序头表

判断是否为2的地方是PT_DYNAMIC (2) 作用就是找到struct program_table_entry32_t program_table_element[3]

image-20250323152409823

31
dd
也就是Dynamic段

Dynamic段是一个包含动态链接信息的段,它被存储在ELF文件的程序头表(Program Header Table)中,通常具有PT_DYNAMIC类型。这个段在动态链接过程中被动态链接器(如ld-linux.so)使用,以解析程序的依赖关系和完成动态链接 主要用于寻找与动态链接相关的其他节( .dynsym .dynstr .hash等节)

image-20250323161529042

那我们也就可以知道

image-20250323153325645

这里就是获取各个节的地址

最后一段的标识符就是4 也就是说v7为hash段的偏移地址

image-20250323155637106

继续看就是对字符串进行的操作

手动模拟一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdint.h>

int main(){
char chr[]="ooxx";
uint8_t* a1=chr;
unsigned __int8 *v1; // r1
unsigned int v3; // [sp+8h] [bp-Ch]


v3 = 0;
while ( *a1 )
{
v1 = a1++;
v3 = (((*v1 + 16 * v3) & 0xF0000000) >> 24) ^ (*v1 + 16 * v3) & 0xF0000000 ^ (*v1 + 16 * v3);
}

printf("%x",v3);


}

image-20250323160253294

image-20250323161812919

就是根据hash值与nbucket取模作为bucket链的索引,bucket[hash % nbucket]的值作为.dynsym的索引获得动态链接符号表(Elf32_Sym),从符号表的st_name找到.dynstr中对应的字符串与函数名相比较,若不等,则根据bucket[hash % nbucket]的值X作为chain链的索引,chain[X]的值重新获取一个动态链接符号表,拿到字符串索引后获取.dynstr中对应的字符串与函数名相比较,若再不等,继续根据chain[X]的值Y作为chain链的索引,chain[Y]的值重新获取一个动态链接符号表,直到找到或者chain终止为止。代码实现如下

1
2
3
4
5
6
for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){  
if(strcmp(dynstr + (dynsym + i)->st_name, funcName) == 0){
flag = 0;
break;
}
}

函数Hash值在74行代码中已经计算得到0x766F8,nbucket=0x107,mod为hash % nbucket = 140,因为hash表的前两个元素是nbucket和nchain,每个元素是Elf32_Word类型,大小为4,所以bucket[hash % nbucket]是第(140 + 2) * 4 = 568号字节,其值为0x19B

34

0x19B做为.dynsym动态链接符号表(Elf32_Sym)的索引,Elf32_Sym对象大小为16字节,所以在符号表的位置为0x19B * 16 = 6576号字节,st_name是Elf32_Sym对象的第一个元素,所以其值为0x1617

35

.dynstr字符串表的offset等于0x1D00

36

st_name为索引的字符串位置则等于0x1D00 + 0x1617 = 0x3317,对应字符串”_ZTIPn”,与ooxx不等。所以需要计算chain[0x19B]的值。先计算chain的起始位置为(nbucket + 2) * 4,nbucket = 0x107,所以chain的起始位置为1060号字节,0x19B十进制为411,那chain链的411索引对应的字节应该是1060 + 411 * 4 = 2704号字节,值为0x5D

37

对应.dynsym动态链接符号表的位置为0x5D * 16 = 1488号字节,st_name = 0x214

38

对应的字符串地址为0x1D00 + 0x214 = 0x1F14,字符串值为”ooxx”,是我们需要查找的符号。结合上图,则可知Elf32_Sym对象的st_value = 0x8DC5,st_size = 0x248

39

牛魔的 复现都要死在这了

image-20250323165029683

现在需要找到的就是key的数值

image-20250323165104299

bss段存放的是需要加载的数据 我们只能从程序运行的内存中dump出内容 进行加解密

先查找出so的加载的基地址再使用ida中的偏移地址获取key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//获取用于异或解密的密钥
Java.perform(function () {
var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
//1.获取app的文件目录files,/data/user/0/$package_name/files/
var dir = currentApplication.getApplicationContext().getFilesDir().getPath();
//2.获取so文件信息
var libso = Process.getModuleByName("libnative-lib.so");
var byte_1C180_addr = 0x1C180
//3.获取用于异或解密的密钥的起始地址
var xor_array_base = ptr(Number(libso.base) + byte_1C180_addr)
//4.用于异或解密的密钥的大小
var xor_array_size = 464;
console.log("[xor_array_base]: ", xor_array_base);
console.log("[xor_array_size]: ", ptr(xor_array_size));
//5.拼接密钥文件路径

//7.修改密钥所在内存的属性为可读
Memory.protect(xor_array_base, xor_array_size, 'r');
//8.读取密钥到缓冲区
var xor_array_buffer = xor_array_base.readByteArray(xor_array_size);
//9.把缓冲区内容写入文件

console.log("[dump]: ", xor_array_buffer );

});

image-20250323171438518

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
key = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF]
def patchFunc(addr,size,key):
for i in range(size):
# 从addr处读取1个字节的内容
byte = get_bytes(addr + i, 1)
# 异或运算解密
decodeBuf = ord(byte) ^ key[i]
print("i: %d, addr: %s, bytes_hex: %s, decode_bytes_hex: %s" % (i,hex(addr + i),hex(ord(byte)),hex(decodeBuf)))
# 将addr地址处patch成decodeBuf的内容
patch_byte(addr + i, decodeBuf)
patchFunc(0x8e00,464,key)

image-20250323171533363

修复完成